package org.ecpkn.PAGridSim.core.gridsimEntity.policy;

import eduni.simjava.Sim_event;
import eduni.simjava.Sim_system;
import gridsim.AllocPolicy;
import gridsim.GridSim;
import gridsim.GridSimTags;
import gridsim.Gridlet;
import gridsim.PE;
import gridsim.ResGridlet;
import gridsim.ResGridletList;

import java.util.Calendar;
import java.util.Iterator;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ecpkn.PAGridSim.core.gridsimEntity.paMachine.PAMachine;
import org.ecpkn.PAGridSim.core.report.ExecutionTraceHashMap;
import org.ecpkn.PAGridSim.core.report.SingleExecutionTrace;
import org.ecpkn.PAGridSim.core.tasksCreation.Job;

/**
 * This class extends the class AllocPolicy and implements its abstract methods,
 * but we don't consider the following cases demanded by the user:
 * gridletPause,gridletMove,gridletResume,gridletCancel for simplify the
 * situation. We focus on how to respond to a Gridlet submitted in this
 * GridResource with space-share method. This class is adapted from
 * SpaceShared.java in GridSim project. It is a basic and simple scheduler that
 * runs each Gridlet to one Processing Element (PE). If a Gridlet requires more
 * than one PE, then this scheduler only assign this Gridlet to one PE.
 * 
 * @author Lin ZHOU
 */

public abstract class BasicPolicy extends AllocPolicy {

	private static final Logger logger = LogManager
			.getLogger(BasicPolicy.class);

	protected ResGridletList gridletQueueList_; // Queue list
	protected ResGridletList gridletInExecList_; // Execution list
	protected ResGridletList gridletPausedList_; // Pause list

	protected double lastUpdateTime_; // the last time Gridlets updated

	/**
	 * Allocates a new BasicPolicy object
	 * 
	 * @param resourceName
	 *            the GridResource entity name that will contain this allocation
	 *            policy
	 * @param entityName
	 *            this object entity name
	 * @throws Exception
	 *             This happens when one of the following scenarios occur:
	 *             <ul>
	 *             <li>creating this entity before initializing GridSim package
	 *             <li>this entity name is <tt>null</tt> or empty
	 *             <li>this entity has <tt>zero</tt> number of PEs (Processing
	 *             Elements). <br>
	 *             No PEs mean the Gridlets can't be processed. A GridResource
	 *             must contain one or more Machines. A Machine must contain one
	 *             or more PEs.
	 *             </ul>
	 * @see gridsim.GridSim#init(int, Calendar, boolean, String[], String[],
	 *      String)
	 * @pre resourceName != null
	 * @pre entityName != null
	 * @post $none
	 */
	public BasicPolicy(String resourceName, String entityName) throws Exception {
		super(resourceName, entityName);
		this.gridletInExecList_ = new ResGridletList();
		this.gridletPausedList_ = new ResGridletList();
		this.gridletQueueList_ = new ResGridletList();
		this.lastUpdateTime_ = 0.0;
	}

	/**
	 * This main purpose of this method is to handle internal events, i.e.
	 * events that are sent to the same entity itself. It mainly acts as a time
	 * keeper since GridSim is a discreet-event simulation.
	 * 
	 * @pre $none
	 * @post $none
	 */
	public void body() {
		// a loop that is looking for internal events only
		Sim_event ev = new Sim_event();

		while (Sim_system.running()) {

			super.sim_get_next(ev);

			// if the simulation finishes then exit the loop
			if (ev.get_tag() == GridSimTags.END_OF_SIMULATION
					|| super.isEndSimulation() == true) {
				break;
			}

			// Internal Event if the event source is this entity
			if (ev.get_src() == super.myId_ && gridletInExecList_.size() > 0) {
				updateGridletProcessing(); // update Gridlets
				checkGridletCompletion(); // check for finished Gridlets
			}
		}
		// CHECK for ANY INTERNAL EVENTS WAITING TO BE PROCESSED

		while (super.sim_waiting() > 0) {
			// wait for event and ignore since it is likely to be related to
			// internal event scheduled to update Gridlets processing
			super.sim_get_next(ev);
			logger.info(super.resName_
					+ ".BasicPolicy.body(): ignore internal events");
		}
	}

	/**
	 * Schedules a new Gridlet that has been received by the GridResource
	 * entity.
	 * 
	 * @param gl
	 *            a Gridlet object that is going to be executed
	 * @param ack
	 *            an acknowledgement, i.e. <tt>true</tt> if wanted to know
	 *            whether this operation is success or not, <tt>false</tt>
	 *            otherwise (don't care)
	 * @pre gl != null
	 * @post $none
	 */
	public void gridletSubmit(Gridlet gl, boolean ack) {

		// update the current Gridlets in exec list up to this point in time
		updateGridletProcessing();
		ResGridlet rgl = new ResGridlet(gl);
		schedule(rgl);
		// Sends back the Gridlet object to its user or owner
		super.sendFinishGridlet(gl);
		// sends back an ack if required
		if (ack) {
			super.sendAck(GridSimTags.GRIDLET_SUBMIT_ACK, true,
					gl.getGridletID(), gl.getUserID());
		}
	}

	/**
	 * The schedule policy. It's an abstract method, and it's different in
	 * different concrete child class
	 * 
	 * @param rgl
	 *            the incoming Gridlet needed to be scheduled
	 */
	protected abstract void schedule(ResGridlet rgl);

	/**
	 * Allocate PE to Gridlet. It's an abstract method. the parameter
	 * taskMachineID is only useful in a partition allocation approach, you can
	 * ignore it in the global allocation mode. This method is called in two
	 * cases: when a Gridlet is submitted(there may be a preemption if
	 * authorized) or when a Gridlet finishes and we allocate the queue or
	 * paused list
	 * 
	 * @param rgl
	 *            the incoming Gridlet needed to be scheduled
	 * @param taskMachineID
	 *            a Gridlet job belongs to a Task, and the Task may have been
	 *            allocated to a certain machine. In partition mode, we should
	 *            keep its jobs in the same machine
	 * @param freeMachineID
	 *            the machine which is available now
	 * @return succeed(true) or not(false)
	 */
	protected abstract boolean allocatePEtoGridlet(ResGridlet rgl,
			int taskMachineID, int freeMachineID);

	/**
	 * Allocates a Gridlet in the Queue list or the Pause list (if any) to
	 * execution list. Different strategy in concrete children classes.
	 * 
	 * @param freeMachineID
	 *            the machine which is available now
	 */
	protected abstract void allocateQueueAndPauseGridlet(int freeMachineID);

	/**
	 * Updates the execution of all Gridlets for a period of time. The time
	 * period is determined from the last update time up to the current time.
	 * Once this operation is successful, then the last update time refers to
	 * the current time.
	 */
	protected void updateGridletProcessing() {
		// Identify MI share for the duration (from last event time)
		double time = GridSim.clock();
		double timeSpan = time - lastUpdateTime_;
		// if current time is the same or less than the last update time,
		// then ignore
		if (timeSpan <= 0.0) {
			return;
		}
		// Update Current Time as Last Update
		lastUpdateTime_ = time;
		double load = 0.0;
		// if no Gridlets in execution then ignore the rest
		if (gridletInExecList_.size() == 0) {
			return;
		}
		ResGridlet obj = null;
		// a loop that allocates MI share for each Gridlet accordingly
		Iterator<?> iter = gridletInExecList_.iterator();
		while (iter.hasNext()) {
			obj = (ResGridlet) iter.next();
			// Updates the Gridlet length that is currently being executed
			load = getMIShare(timeSpan, obj.getMachineID());
			obj.updateGridletFinishedSoFar(load);
		}
	}

	/**
	 * Not yet used. Identifies MI share (max and min) each Gridlet gets for a
	 * given timeSpan
	 * 
	 * @param timeSpan
	 *            duration
	 * @param machineId
	 *            machine ID that executes this Gridlet
	 * @return the total MI share that a Gridlet gets for a given
	 *         <tt>timeSpan</tt>
	 * @pre timeSpan >= 0.0
	 * @pre machineId > 0
	 * @post $result >= 0.0
	 */
	private double getMIShare(double timeSpan, int machineId) {
		// each Machine might have different PE Rating compare to another
		// so much look at which Machine this PE belongs to
		double totalMI = this.resource_.getMachineList().getMachine(machineId)
				.getMIPSRating()
				* timeSpan * 1;
		return totalMI;
	}

	/**
	 * pause a rgl from a machine. General without consideration of global or
	 * partition policy
	 * 
	 * @param rgl
	 *            the rgl to be paused
	 * @param taskMachineID
	 *            the machine that will be free
	 * @return succeed or not(boolean)
	 */
	protected boolean resumeGridletFromPause(ResGridlet rgl, int taskMachineID) {

		rgl.setGridletStatus(Gridlet.RESUMED);
		// update the Gridlets up to this point in time
		updateGridletProcessing();

		// it should succeed
		boolean success = allocatePEtoGridlet(rgl, taskMachineID, taskMachineID);

		if (success == false) {
			// it never happen if we allow missing deadline job to continue
		} else {
			gridletPausedList_.remove(gridletPausedList_.indexOf(rgl));
			return true;
		}
		return false;
	}

	/**
	 * Forecast finish time of a Gridlet.
	 * <tt>Finish time = length / available rating</tt>
	 * 
	 * @param availableRating
	 *            the shared MIPS rating for all Gridlets
	 * @param length
	 *            remaining Gridlet length
	 * @return Gridlet's finish time.
	 * @pre availableRating >= 0.0
	 * @pre length >= 0.0
	 */
	protected double forecastFinishTime(double availableRating, double length) {
		double finishTime = (length / availableRating);
		// This is as a safeguard since the finish time can be extremely
		// small close to 0.0, such as 4.54E-14. Hence causing some Gridlets
		// never to be finished and consequently hang the program
		if (finishTime < 0.001) {
			finishTime = 0.001;
		}
		return finishTime;
	}

	/**
	 * Checks all Gridlets in the execution list whether they are finished
	 */
	private void checkGridletCompletion() {
		ResGridlet obj = null;
		int i = 0;
		while (i < gridletInExecList_.size()) {
			obj = (ResGridlet) gridletInExecList_.get(i);
			if (obj.getRemainingGridletLength() == 0.0) {
				gridletInExecList_.remove(obj);
				gridletFinish(obj, Gridlet.SUCCESS);
				continue;
			}
			i++;
		}
		// if there are still Gridlets left in the execution
		// then send this into itself for an hourly interrupt
		if (gridletInExecList_.size() > 0) {
			super.sendInternalEvent(60.0 * 60.0);
		}
	}

	/**
	 * Updates the Gridlet's properties, such as status once a Gridlet is
	 * considered finished.
	 * 
	 * @param rgl
	 *            a ResGridlet object
	 * @param status
	 *            the Gridlet status
	 * @pre rgl != null
	 * @pre status >= 0
	 */
	protected void gridletFinish(ResGridlet rgl, int status) {
		// Set PE on which Gridlet finished to FREE
		super.resource_.setStatusPE(PE.FREE, rgl.getMachineID(), rgl.getPEID());
		recordExecutionTrace(rgl, rgl.getGridletFinishTime());
		if (((Job) rgl.getGridlet()).getDeadline_() < rgl
				.getGridletFinishTime()) {
			status = Gridlet.FAILED;
		}
		// the order is important! Set the status first then finalize
		// due to timing issues in ResGridlet class
		rgl.setGridletStatus(status);
		rgl.finalizeGridlet();
		allocateQueueAndPauseGridlet(rgl.getMachineID());
	}

	/**
	 * When a part of a resGridlet ends its execution,record its execution trace
	 * 
	 * @param resGridlet
	 * @param endTimeOfTrace
	 */
	protected void recordExecutionTrace(ResGridlet rgl, double endTimeOfTrace) {
		String key = String.valueOf(rgl.getGridletID());
		ExecutionTraceHashMap trace = ((PAMachine) resource_.getMachineList()
				.getMachine(rgl.getMachineID())).getExecutionTraceHashMap();
		while (trace.containsKey(key)) {
			key = key + "*";
		}
		SingleExecutionTrace lt = new SingleExecutionTrace(rgl.getMachineID(),
				rgl.getExecStartTime(), endTimeOfTrace,
				((Job) rgl.getGridlet()).getDeadline_());
		trace.put(key, lt);
	}

	/**
	 * Not yet used. Finds the status of a specified Gridlet ID.
	 * 
	 * @param gridletId
	 *            a Gridlet ID
	 * @param userId
	 *            the user or owner's ID of this Gridlet
	 * @return the Gridlet status or <tt>-1</tt> if not found
	 * @see gridsim.Gridlet
	 * @pre gridletId > 0
	 * @pre userId > 0
	 * @post $none
	 */
	public synchronized int gridletStatus(int gridletId, int userId) {
		ResGridlet rgl = null;

		// Find in EXEC List first
		int found = gridletInExecList_.indexOf(gridletId, userId);
		if (found >= 0) {
			// Get the Gridlet from the execution list
			rgl = (ResGridlet) gridletInExecList_.get(found);
			return rgl.getGridletStatus();
		}

		// Find in Paused List
		found = gridletPausedList_.indexOf(gridletId, userId);
		if (found >= 0) {
			// Get the Gridlet from the execution list
			rgl = (ResGridlet) gridletPausedList_.get(found);
			return rgl.getGridletStatus();
		}

		// Find in Queue List
		found = gridletQueueList_.indexOf(gridletId, userId);
		if (found >= 0) {
			// Get the Gridlet from the execution list
			rgl = (ResGridlet) gridletQueueList_.get(found);
			return rgl.getGridletStatus();
		}

		// if not found in all 3 lists then no found
		return -1;
	}

	@Override
	public void gridletCancel(int arg0, int arg1) {
		// not yet implement
	}

	@Override
	public void gridletMove(int arg0, int arg1, int arg2, boolean arg3) {
		// not yet implement
	}

	@Override
	public void gridletPause(int arg0, int arg1, boolean arg2) {
		// not yet implement
	}

	@Override
	public void gridletResume(int arg0, int arg1, boolean arg2) {
		// not yet implement
	}

}
